/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package genomemap.data;

import com.google.common.collect.*;
import commons.core.Data;
import commons.core.MutableData;
import commons.util.ArrayUtil;
import commons.util.BinaryDataUtil;
import commons.util.ExceptionUtil;
import genomemap.data.TAB08Data.Genotype;
import genomemap.model.TAB08;

import java.util.*;

/**
 * @author Susanta Tewari
 * @since Jul 30, 2011
 */
public class TAB08Data extends Data<TAB08, TAB08Data> implements Iterable<Genotype> {

    /** Field description */
    private ImmutableMap<String, Genotype> genotypes;

    /**
     * @param m model
     * @param scores each row indicates a new progeny; the scores in a progeny correspond to the
     * markers in the model; may contain duplicate scores
     */
    public TAB08Data(final TAB08 m, final byte[][] scores) {

        super(m);


        // create a multiset with scores
        final Multiset<Genotype> multiset = HashMultiset.create(scores.length);


        // the label of the genotype reflects its order
        int counter = 0;

        for (final byte[] val : scores) {

            counter++;

            multiset.add(new Genotype("" + counter, val, 1));
        }

        final Set<Genotype> elementSet  = multiset.elementSet();
        final Map<String, Genotype> map = new HashMap<>(elementSet.size());


        // collect the similar genotypes, set the label of the distict genotype to be list
        // of all the genotypes including itself
        int geneCount = m.getGenes().size();

        for (final Genotype element : elementSet) {

            String gene = element.getLabel();

            for (final Genotype original : multiset) {

                if (original.equals(element)) {
                    gene += "," + original.getLabel();
                }
            }

            assert geneCount == element.getScore().length;


            // now add the genotype
            final Genotype genotype = new Genotype(gene, element.getScore(),
                                          multiset.count(element));

            map.put(gene, genotype);
        }


        // make the genotypes unmodifiable
        genotypes = ImmutableMap.copyOf(map);
    }

    /**
     * @param m model
     * @param progenyGenotype labels are progeny
     * @param progenyFreq progeny frequency
     */
    public TAB08Data(final TAB08 m, final Map<String, String> progenyGenotype,
                     final Map<String, Integer> progenyFreq) {

        super(m);


        // create an order for the labels
        final List<String> progeny = new ArrayList<>(progenyGenotype.keySet());


        // using the order of labels, get the list of freq
        final List<Integer> freq = new ArrayList<>(progeny.size());

        for (final String offspring : progeny) {
            freq.add(progenyFreq.get(offspring));
        }


        // using the label order, create scores
        final byte[][] scores = BinaryDataUtil.getBinaryData(progeny, progenyGenotype);

        init(progeny, scores, freq);
    }

    /**
     *
     * @param m
     * @param progeny progeny
     * @param scores should not contain duplicates; if it does they will be silently skipped
     * @param freq frequency
     */
    public TAB08Data(final TAB08 m, final List<String> progeny, final byte[][] scores,
                     final List<Integer> freq) {

        super(m);

        init(progeny, scores, freq);
    }

    /**
     * Method description
     *
     *
     * @param progeny
     * @param scores
     * @param freq
     */
    private void init(final List<String> progeny, final byte[][] scores, final List<Integer> freq) {

        final Map<String, Genotype> map = new HashMap<>(scores.length);

        for (int i = 0; i < scores.length; i++) {

            final String offspring  = progeny.get(i);
            final Genotype genotype = new Genotype(progeny.get(i), scores[i], freq.get(i));

            map.put(offspring, genotype);
        }

        genotypes = ImmutableMap.copyOf(map);
    }

    /**
     * Method description
     *
     *
     * @return
     */
    private ImmutableSet<String> getProgeny() {
        return genotypes.keySet();
    }

    /**
     * Method description
     *
     *
     * @param offspring
     *
     * @return
     */
    private Genotype getGenotype(final String offspring) {

        checkoffspringArgument(offspring);

        return genotypes.get(offspring);
    }

    /**
     * Method description
     *
     *
     * @param offspring
     */
    private void checkoffspringArgument(final String offspring) {

        if (!genotypes.containsKey(offspring)) {
            ExceptionUtil.throwIllegalArgEx(getClass(), "TAB08Data_Argument_Gene_Not_Found");
        }
    }

    /**
     * Method description
     *
     *
     * @return
     */
    @Override
    public MutableData<TAB08, TAB08Data> getMutable() {

        return new MutableData<TAB08, TAB08Data>(getModel()) {

            @Override
            public TAB08Data getImmutable() {

                final TAB08Data old_data             = TAB08Data.this;
                final int[] geneIndex = ArrayUtil.indicesOf(old_data.getModel().getGenes(),
                                            getModel().getGenes());
                final List<String> old_progeny       = new ArrayList<>(old_data.getProgeny());
                final List<Integer> old_progeny_freq = new ArrayList<>(old_progeny.size());
                final byte[][] new_scores            = new byte[old_progeny.size()][];

                for (int i = 0; i < old_progeny.size(); i++) {

                    final String offspring  = old_progeny.get(i);
                    final Genotype genotype = old_data.getGenotype(offspring);

                    new_scores[i] = genotype.getScore(geneIndex);

                    old_progeny_freq.add(i, genotype.getFreq());
                }

                return new TAB08Data(getModel(), old_progeny, new_scores, old_progeny_freq);
            }
        };
    }

    /**
     * Method description
     *
     *
     * @return
     */
    @Override
    public UnmodifiableIterator<Genotype> iterator() {
        return genotypes.values().iterator();
    }

    /**
     * Method description
     *
     *
     * @return
     */
    public int genotypeCount() {
        return genotypes.size();
    }

    /**
     * Method description
     *
     *
     * @return
     */
    @Override
    public int getSampleSize() {

        int total = 0;

        for (final Genotype genotype : this) {
            total += genotype.getFreq();
        }

        return total;
    }

    /**
     * Class description
     *
     *
     * @version        Enter version here..., 12/10/09
     * @author         Susanta Tewari
     */
    public static class Genotype {

        /** Field description */
        private final byte[] score;

        /** Field description */
        private final String label;

        /** Field description */
        private final int freq;

        /**
         * Constructs ...
         *
         *
         * @param label
         * @param score
         * @param freq
         */
        public Genotype(final String label, final byte[] score, final int freq) {

            this.label = label;
            this.score = score;
            this.freq  = freq;
        }

        /**
         * Method description
         *
         *
         * @return
         */
        public int length() {
            return score.length;
        }

        /**
         * Method description
         *
         *
         * @return
         */
        public int getFreq() {
            return freq;
        }

        /**
         * Method description
         *
         *
         * @return
         */
        public String getLabel() {
            return label;
        }

        /**
         * Method description
         *
         *
         * @return
         */
        public byte[] getScore() {
            return score;
        }

        /**
         * Indices in probeIndex correspond to a subset of genes.
         *
         * @param geneIndex gene index in the underlying storage of genes
         * @return a binary score (1 or 0) for the specified gene
         */
        private byte[] getScore(final int[] geneIndex) {

            final byte[] result = new byte[geneIndex.length];

            for (int i = 0; i < result.length; i++) {
                result[i] = score[geneIndex[i]];
            }

            return result;
        }

        /**
         * Method description
         *
         *
         * @param obj
         *
         * @return
         */
        @Override
        public boolean equals(final Object obj) {

            if (obj == null) {
                return false;
            }

            if (getClass() != obj.getClass()) {
                return false;
            }

            final Genotype other = (Genotype) obj;

            return Arrays.equals(this.score, other.score);
        }

        /**
         * Method description
         *
         *
         * @return
         */
        @Override
        public int hashCode() {

            int hash = 7;

            hash = 17 * hash + Arrays.hashCode(this.score);

            return hash;
        }
    }
}
